﻿using DZParallelLib.DZImage;
using DZParallelLib.Utility;
using Nito.AsyncEx;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using VerifyArgs;

namespace DZParallelLib
{
    /// <summary>
    /// Main class to render DeepZoom collections.
    /// </summary>
    /// <remarks>
    /// Before calling this all the sub-images should have been rendered already,
    /// and the main DSC file should have been created.
    /// </remarks>
    public class RenderDZCollectionImage
    {
        /// <summary>
        /// Initalize default settings for rendering the image.
        /// </summary>
        public RenderDZCollectionImage()
        {
            NumberOfLevels = 9;
            TileSize = 256;
        }

        /// <summary>
        /// Render a DZ collection.
        /// </summary>
        /// <param name="DSCFile">The XML file that describes the DZ collection.</param>
        /// <param name="DZOutputDirectory">Directory where the collection tiles should be written</param>
        /// <param name="renderOnlyIDs">Only the ID's given in the render list will be rendered</param>
        public async Task RenderImage(FileInfo DSCFile, DirectoryInfo DZOutputDirectory, HashSet<ulong> renderOnlyIDs = null)
        {
            Verify.Args(new { DSCFile }).NotNull().Exists();
            Verify.Args(new { DZOutputDirectory }).NotNull();

            if (renderOnlyIDs == null)
                Console.WriteLine("Nothign to work on");
            else
                Console.WriteLine("Working on {0} items to render.", renderOnlyIDs.Count);

            // Load in the DZ image for processing
            var info = DZIO.LoadDZ(DSCFile);

            // Go after only the items that we have been requested to re-draw.

            var allItems = info.Items as IEnumerable<Data.CollectionI>;
            if (renderOnlyIDs != null)
                allItems = allItems.Where(i => renderOnlyIDs.Contains(i.Id));

            // Create the cache that will keep highly trafficed images in memory. This will end up
            // holding onto a number of resources that must be cleaned up to prevent leaks.

            var imageTileCache = new ImageLevelCache(
            info.Items.Length > 0 ? (int)info.Items.Select(i => i.Id).Max() : 10,
            NumberOfLevels,
            (int)TileSize,
            DZOutputDirectory);

            // Now, for every single item in there, we will need to construct an image.

            try
            {
                var allTasks = allItems.Select(item => Task.Run(async () => await EmitImage(DSCFile.Directory, item, imageTileCache))).ToArray();
                await Task.WhenAll(allTasks);

                // Clean everything up.

                await imageTileCache.CleanUp();
            }
            finally
            {
                // Clean up everything we are holding onto in memory if we've  nto already. It shoudl go very fast
                // if not.

                imageTileCache.CleanUp().Wait();
            }
        }

        /// <summary>
        /// Gets or sets the number of levels we will generate for this image. By default it is
        /// set to 8 the number of levels that will get you the current tile size as a max.
        /// </summary>
        public int NumberOfLevels { get; set; }

        /// <summary>
        /// Size in pixes of each tile. Defaults to 256. Should be a power of two.
        /// </summary>
        /// <remarks>
        /// No checking is done to make sure this is a power of 2. Also, the NumberOfLevels should be set
        /// to something so that you get to this.
        /// </remarks>
        public uint TileSize { get; set; }

        /// <summary>
        /// Keep the number of guys running an Emit limited.
        /// </summary>
        public AsyncSemaphore _keepItLimited = new AsyncSemaphore(8);

        /// <summary>
        /// Generate the small DZ guy for a single image.
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        private async Task EmitImage(DirectoryInfo baseDir, Data.CollectionI item, ImageLevelCache cache)
        {
            await _keepItLimited.WaitAsync();
            try
            {
                // Load the largest version of the image we are ever going to need
                // from the already rendered DZ output.

                var sourceImage = await new DZRenderedImage(new FileInfo(Path.Combine(baseDir.FullName, item.Source))).GetClosestBitmap(TileSize);

                //
                // Next, find the location (using the morton number) of where this image
                // sits in the grid.
                //

                var crLocation = CalculateImageCRLocation(item.Id);

                //
                // Loop over all the levels we are going to generate tiles for.
                //

                for (int level = 0; level < NumberOfLevels; level++)
                {
                    // What file should the tile go into?
                    var outputTileIndex = CalculateTile(level, crLocation);

                    var imageLocPixels = CalculateTilePixelOffset(level, crLocation);

                    var imageScaling = CalculateImageScaling(level, sourceImage.Width, sourceImage.Height);
                    if (imageScaling > 1.0)
                        imageScaling = 1.0;
                    var imageLocRect = new Rectangle(imageLocPixels.X, imageLocPixels.Y,
                        (int)Math.Ceiling((sourceImage.Width * imageScaling)),
                        (int)Math.Ceiling((sourceImage.Height * imageScaling))
                        );

                    // Load up the current tile that is on disk, put in the image
                    // and write the tile back out.

                    await cache.UpdateImage(level, outputTileIndex, g => g.DrawImage(sourceImage, imageLocRect));
                }
            }
            finally
            {
                _keepItLimited.Release();
                GC.Collect();
            }
        }

        /// <summary>
        /// Calculate the scaling to get the image with w and h to the approparite size for this
        /// level.
        /// </summary>
        /// <param name="level"></param>
        /// <param name="w"></param>
        /// <param name="h"></param>
        /// <returns>Scale factor for this image at the given level.</returns>
        internal double CalculateImageScaling(int level, int w, int h)
        {
            var mh = (double)Math.Max(w, h);
            var bestLevel = (int)Math.Ceiling(Math.Log(mh, 2));
            return 1.0 / Math.Pow(2, bestLevel - level);
        }

        /// <summary>
        /// Calculate the offset within a tile at which to write the pixel.
        /// </summary>
        /// <param name="level"></param>
        /// <param name="crLocation"></param>
        /// <returns></returns>
        internal Point CalculateTilePixelOffset(int level, Tuple<ulong, ulong> crLocation)
        {
            var imagesPerTile = TileSize / CalculateLevelSize(level);
            return new Point(
                (int)((crLocation.Item1 % imagesPerTile) * CalculateLevelSize(level)),
                (int)((crLocation.Item2 % imagesPerTile) * CalculateLevelSize(level))
                );
        }

        /// <summary>
        /// What is the tile index (column, row) that this image should be placed for this row?
        /// </summary>
        /// <param name="level"></param>
        /// <param name="crLocation"></param>
        /// <returns></returns>
        internal Tuple<ulong, ulong> CalculateTile(int level, Tuple<ulong, ulong> crLocation)
        {
            var levelSize = CalculateLevelSize(level);
            Func<ulong, ulong> cLoc = loc => (ulong)Math.Floor((loc * levelSize) / (double)TileSize);
            return Tuple.Create(cLoc(crLocation.Item1), cLoc(crLocation.Item2));
        }

        /// <summary>
        /// Size of a particular level in pixels
        /// </summary>
        /// <param name="level"></param>
        /// <returns></returns>
        internal uint CalculateLevelSize(int level)
        {
            return (uint)Math.Pow(2, level);
        }

        /// <summary>
        /// Calculate the column, row pair (x,y) for the image given its iD number.
        /// </summary>
        /// <param name="order"></param>
        /// <returns>A Tuple of column,row.</returns>
        internal Tuple<ulong, ulong> CalculateImageCRLocation(ulong order)
        {
            ulong column = 0;
            ulong row = 0;

            for (int i = 0; i < 32; i += 2)
            {
                int offset = i / 2;

                int column_offset = i;
                ulong column_mask = (ulong)1 << column_offset; // (ulong)Math.Pow(2, column_offset);
                ulong column_value = (order & column_mask) >> column_offset;
                column |= column_value << offset;

                int row_offset = i + 1;
                ulong row_mask = (ulong)1 << row_offset;
                ulong row_value = (order & row_mask) >> row_offset;
                row |= row_value << offset;
            }

            return Tuple.Create(column, row);
        }
    }
}
